@miso.ai/doggoganger 0.9.0-beta.5 → 0.9.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/index.js +6 -2
- package/package.json +1 -1
- package/src/api/answers.js +59 -36
- package/src/api/index.js +9 -7
- package/src/data/articles.js +6 -29
- package/src/data/fields.js +49 -0
- package/src/data/index.js +1 -0
- package/src/data/lorem.js +55 -11
- package/src/data/markdown/index.js +233 -0
- package/src/data/products.js +9 -30
- package/src/data/utils.js +14 -11
- package/src/index.js +3 -2
package/cli/index.js
CHANGED
|
@@ -21,6 +21,10 @@ let { watch, ...argv } = yargs(hideBin(process.argv))
|
|
|
21
21
|
describe: 'Watch files for changes',
|
|
22
22
|
type: 'boolean',
|
|
23
23
|
})
|
|
24
|
+
.option('answer-format', {
|
|
25
|
+
describe: 'Answer field format in answers API response',
|
|
26
|
+
type: 'string',
|
|
27
|
+
})
|
|
24
28
|
.option('serve', {
|
|
25
29
|
alias: 's',
|
|
26
30
|
describe: 'Serve static files as well',
|
|
@@ -28,8 +32,8 @@ let { watch, ...argv } = yargs(hideBin(process.argv))
|
|
|
28
32
|
})
|
|
29
33
|
.argv;
|
|
30
34
|
|
|
31
|
-
const { port, serve } = argv;
|
|
32
|
-
argv = { port, serve };
|
|
35
|
+
const { port, serve, ['answer-format']: answerFormat } = argv;
|
|
36
|
+
argv = { port, serve, answerFormat };
|
|
33
37
|
|
|
34
38
|
if (watch) {
|
|
35
39
|
const exec = `node ${resolve(__dirname, 'server.js')} ${JSON.stringify(JSON.stringify(argv))}`;
|
package/package.json
CHANGED
package/src/api/answers.js
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import Router from '@koa/router';
|
|
2
2
|
import { v4 as uuid } from 'uuid';
|
|
3
|
-
import { lorem, articles, utils } from '../data/index.js';
|
|
3
|
+
import { lorem, md, articles, utils } from '../data/index.js';
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const { randomInt } = utils;
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const CPS = 100;
|
|
8
8
|
const ITEMS_LOADING_TIME = 3;
|
|
9
9
|
const STAGES = [
|
|
10
10
|
{
|
|
11
|
+
name: 'fetch',
|
|
11
12
|
duration: 1.5,
|
|
12
|
-
text: `
|
|
13
|
+
text: `Checking the question and fetching results...`,
|
|
13
14
|
},
|
|
14
15
|
{
|
|
16
|
+
name: 'verify',
|
|
17
|
+
duration: 1.5,
|
|
18
|
+
text: `Verifying results...`,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'generate',
|
|
15
22
|
duration: 1.5,
|
|
16
23
|
text: `Generating answer...`,
|
|
17
|
-
}
|
|
24
|
+
},
|
|
18
25
|
];
|
|
19
26
|
|
|
20
27
|
function formatDatetime(timestamp) {
|
|
@@ -22,25 +29,34 @@ function formatDatetime(timestamp) {
|
|
|
22
29
|
return str.endsWith('Z') ? str.slice(0, -1) : str;
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
function answer(format) {
|
|
33
|
+
switch (format) {
|
|
34
|
+
case 'markdown':
|
|
35
|
+
return md.markdown({});
|
|
36
|
+
case 'plaintext':
|
|
37
|
+
default:
|
|
38
|
+
return lorem.lorem({
|
|
39
|
+
min: 50,
|
|
40
|
+
max: 100,
|
|
41
|
+
decorates: ['description'],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
25
46
|
const answers = new Map();
|
|
26
47
|
|
|
27
48
|
class Answer {
|
|
28
49
|
|
|
29
|
-
constructor(question, previous_question_id) {
|
|
50
|
+
constructor(question, previous_question_id, { answerFormat = 'plaintext' } = {}) {
|
|
30
51
|
this.question_id = uuid();
|
|
31
52
|
this.question = question;
|
|
32
53
|
this.previous_answer_id = previous_question_id;
|
|
33
54
|
this.timestamp = Date.now();
|
|
34
55
|
this.datetime = formatDatetime(this.timestamp);
|
|
35
56
|
|
|
36
|
-
this.answer =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
decorates: ['description'],
|
|
40
|
-
output: 'array',
|
|
41
|
-
});
|
|
42
|
-
this.relatedResources = [...articles({ rows: utils.randomInt(6, 8) })];
|
|
43
|
-
this.sources = [...articles({ rows: utils.randomInt(4, 6) })];
|
|
57
|
+
this.answer = answer(answerFormat);
|
|
58
|
+
this.relatedResources = [...articles({ rows: randomInt(6, 8) })];
|
|
59
|
+
this.sources = [...articles({ rows: randomInt(4, 6) })];
|
|
44
60
|
|
|
45
61
|
this.previous_question_id = previous_question_id && answers.get(previous_question_id) || undefined;
|
|
46
62
|
answers.set(this.question_id, this);
|
|
@@ -49,13 +65,14 @@ class Answer {
|
|
|
49
65
|
get() {
|
|
50
66
|
const now = Date.now();
|
|
51
67
|
const elapsed = (now - this.timestamp) / 1000;
|
|
52
|
-
const [answer, finished] = this._answer(elapsed);
|
|
68
|
+
const [stage, answer, finished] = this._answer(elapsed);
|
|
53
69
|
const sources = this._sources(elapsed);
|
|
54
70
|
const related_resources = this._relatedResources(elapsed);
|
|
55
71
|
const { question_id, question, datetime, previous_question_id } = this;
|
|
56
72
|
|
|
57
73
|
return {
|
|
58
74
|
affiliation: undefined,
|
|
75
|
+
stage,
|
|
59
76
|
answer,
|
|
60
77
|
datetime,
|
|
61
78
|
finished,
|
|
@@ -71,13 +88,13 @@ class Answer {
|
|
|
71
88
|
for (const stage of STAGES) {
|
|
72
89
|
elapsed -= stage.duration;
|
|
73
90
|
if (elapsed < 0) {
|
|
74
|
-
return [stage.text, false];
|
|
91
|
+
return [stage.name, stage.text, false];
|
|
75
92
|
}
|
|
76
93
|
}
|
|
77
|
-
const
|
|
78
|
-
const finished =
|
|
79
|
-
const text =
|
|
80
|
-
return [text, finished];
|
|
94
|
+
const length = Math.floor(elapsed * CPS);
|
|
95
|
+
const finished = length >= this.answer.length;
|
|
96
|
+
const text = finished ? this.answer : this.answer.slice(0, length);
|
|
97
|
+
return ['result', text, finished];
|
|
81
98
|
}
|
|
82
99
|
|
|
83
100
|
_sources(elapsed) {
|
|
@@ -96,22 +113,28 @@ class Answer {
|
|
|
96
113
|
|
|
97
114
|
}
|
|
98
115
|
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const { id } = ctx.params;
|
|
108
|
-
const answer = answers.get(id);
|
|
109
|
-
if (!answer) {
|
|
110
|
-
ctx.status = 404;
|
|
111
|
-
} else {
|
|
116
|
+
export default function({ answerFormat }) {
|
|
117
|
+
const options = Object.freeze({ answerFormat });
|
|
118
|
+
const router = new Router();
|
|
119
|
+
|
|
120
|
+
router.post('/questions', (ctx) => {
|
|
121
|
+
const { q: question, previous_answer_id } = JSON.parse(ctx.request.body);
|
|
122
|
+
const answerFormat = ctx.get('x-answer-format') || options.answerFormat;
|
|
123
|
+
const answer = new Answer(question, previous_answer_id, { answerFormat });
|
|
112
124
|
const data = answer.get();
|
|
113
125
|
ctx.body = JSON.stringify({ data });
|
|
114
|
-
}
|
|
115
|
-
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
router.get('/questions/:id/answer', (ctx) => {
|
|
129
|
+
const { id } = ctx.params;
|
|
130
|
+
const answer = answers.get(id);
|
|
131
|
+
if (!answer) {
|
|
132
|
+
ctx.status = 404;
|
|
133
|
+
} else {
|
|
134
|
+
const data = answer.get();
|
|
135
|
+
ctx.body = JSON.stringify({ data });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
116
138
|
|
|
117
|
-
|
|
139
|
+
return router;
|
|
140
|
+
}
|
package/src/api/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Router from '@koa/router';
|
|
2
|
-
import
|
|
2
|
+
import _answers from './answers.js';
|
|
3
3
|
import recommendation from './recommendation.js';
|
|
4
4
|
import search from './search.js';
|
|
5
5
|
import interactions from './interactions.js';
|
|
@@ -8,11 +8,13 @@ function use(router, path, middleware) {
|
|
|
8
8
|
router.use(path, middleware.routes(), middleware.allowedMethods());
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
export default function(options) {
|
|
12
|
+
const router = new Router();
|
|
12
13
|
|
|
13
|
-
use(router, '/answers',
|
|
14
|
-
use(router, '/recommendation', recommendation);
|
|
15
|
-
use(router, '/search', search);
|
|
16
|
-
use(router, '/interactions', interactions);
|
|
14
|
+
use(router, '/answers', _answers(options));
|
|
15
|
+
use(router, '/recommendation', recommendation);
|
|
16
|
+
use(router, '/search', search);
|
|
17
|
+
use(router, '/interactions', interactions);
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
return router;
|
|
20
|
+
};
|
package/src/data/articles.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as u from './utils.js';
|
|
2
|
-
import * as
|
|
2
|
+
import * as fields from './fields.js';
|
|
3
3
|
|
|
4
4
|
export function *articles({ rows, ...options } = {}) {
|
|
5
5
|
for (let i = 0; i < rows; i++) {
|
|
@@ -9,39 +9,16 @@ export function *articles({ rows, ...options } = {}) {
|
|
|
9
9
|
|
|
10
10
|
function article({} = {}) {
|
|
11
11
|
const id = u.id();
|
|
12
|
-
const prices = u.repeat(u.price, 1, 2);
|
|
13
|
-
prices.sort();
|
|
14
|
-
const seed = Math.floor(Math.random() * 1000);
|
|
15
12
|
|
|
16
13
|
return {
|
|
17
14
|
product_id: id,
|
|
18
|
-
authors:
|
|
19
|
-
min: 1,
|
|
20
|
-
max: 3,
|
|
21
|
-
fixedStarts: 0,
|
|
22
|
-
decorates: ['title'],
|
|
23
|
-
output: 'array',
|
|
24
|
-
}),
|
|
15
|
+
authors: fields.authors(),
|
|
25
16
|
categories: [],
|
|
26
|
-
tags:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
fixedStarts: 0,
|
|
30
|
-
output: 'array',
|
|
31
|
-
}),
|
|
32
|
-
title: lorem.lorem({
|
|
33
|
-
min: 4,
|
|
34
|
-
max: 10,
|
|
35
|
-
fixedStarts: 0,
|
|
36
|
-
decorates: ['title'],
|
|
37
|
-
}),
|
|
38
|
-
description: lorem.lorem({
|
|
39
|
-
min: 20,
|
|
40
|
-
max: 40,
|
|
41
|
-
decorates: ['description'],
|
|
42
|
-
}),
|
|
17
|
+
tags: fields.tags(),
|
|
18
|
+
title: fields.title({ size: [4, 10] }),
|
|
19
|
+
description: fields.description({ size: [20, 40] }),
|
|
43
20
|
//html,
|
|
44
|
-
cover_image:
|
|
21
|
+
cover_image: fields.image(),
|
|
45
22
|
url: `/products/${id}`,
|
|
46
23
|
};
|
|
47
24
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as lorem from './lorem.js';
|
|
2
|
+
import { imageUrl } from './utils.js';
|
|
3
|
+
|
|
4
|
+
export function image({ size = 300 } = {}) {
|
|
5
|
+
return imageUrl(size);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function authors({ size = [1, 3] } = {}) {
|
|
9
|
+
return lorem.lorem({
|
|
10
|
+
size,
|
|
11
|
+
decorates: ['title'],
|
|
12
|
+
output: 'array',
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function tags({ size = [1, 4] } = {}) {
|
|
17
|
+
return lorem.lorem({
|
|
18
|
+
size,
|
|
19
|
+
output: 'array',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// TODO: categories
|
|
24
|
+
|
|
25
|
+
export function title({ size = [2, 6] } = {}) {
|
|
26
|
+
return lorem.lorem({
|
|
27
|
+
size,
|
|
28
|
+
decorates: ['title'],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function description({ size = [10, 20] } = {}) {
|
|
33
|
+
return lorem.lorem({
|
|
34
|
+
size,
|
|
35
|
+
decorates: ['description'],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function availability() {
|
|
40
|
+
return Math.random() > 0.3 ? 'IN_STOCK' : 'OUT_OF_STOCK';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function price() {
|
|
44
|
+
return Math.floor(Math.random() * 10000) / 100;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function rating() {
|
|
48
|
+
return Math.floor(Math.random() * 500) / 100 + 1;
|
|
49
|
+
}
|
package/src/data/index.js
CHANGED
package/src/data/lorem.js
CHANGED
|
@@ -7,8 +7,8 @@ import { randomInt } from './utils.js';
|
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
8
|
const DEFAULT_WORDS = yaml.load(readFileSync(resolve(__dirname, './words.yaml'), 'utf8'));
|
|
9
9
|
|
|
10
|
-
export function lorem({ decorates = [],
|
|
11
|
-
let iterator = limit(
|
|
10
|
+
export function lorem({ decorates = [], output = 'string', size, min, max, ...options } = {}) {
|
|
11
|
+
let iterator = limit(size || [min, max])(base(options));
|
|
12
12
|
for (const decorate of decorates) {
|
|
13
13
|
iterator = lookup(decorate)(iterator);
|
|
14
14
|
}
|
|
@@ -20,14 +20,15 @@ const FNS = {
|
|
|
20
20
|
array,
|
|
21
21
|
title,
|
|
22
22
|
description,
|
|
23
|
+
multiline,
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
function lookup(fn) {
|
|
26
|
-
return typeof fn === 'string' ? FNS[fn]() : fn
|
|
27
|
+
return typeof fn === 'string' ? FNS[fn]() : fn;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
// base //
|
|
30
|
-
export function *base({ words = DEFAULT_WORDS, fixedStarts =
|
|
31
|
+
export function *base({ words = DEFAULT_WORDS, fixedStarts = 0 } = {}) {
|
|
31
32
|
const wordsLength = words.length;
|
|
32
33
|
for (let i = 0; ; i++) {
|
|
33
34
|
yield words[i < fixedStarts ? i : Math.floor(Math.random() * wordsLength)];
|
|
@@ -43,9 +44,34 @@ export function array() {
|
|
|
43
44
|
return iterator => [...iterator];
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
export function multiline({
|
|
48
|
+
wordsPerLine = {
|
|
49
|
+
avg: 10,
|
|
50
|
+
std: 3,
|
|
51
|
+
min: 1,
|
|
52
|
+
},
|
|
53
|
+
} = {}) {
|
|
54
|
+
return iterator => {
|
|
55
|
+
let slen = gaussMS(wordsPerLine);
|
|
56
|
+
let result = '';
|
|
57
|
+
for (let word of iterator) {
|
|
58
|
+
if (result) {
|
|
59
|
+
if (slen-- === 0) {
|
|
60
|
+
result += '\n';
|
|
61
|
+
slen = gaussMS(wordsPerLine);
|
|
62
|
+
} else {
|
|
63
|
+
result += ' ';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
result += word;
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
46
72
|
// decorators //
|
|
47
|
-
export function limit(
|
|
48
|
-
n =
|
|
73
|
+
export function limit(size = [5, 10]) {
|
|
74
|
+
const n = typeof size === 'number' ? size : randomInt(...size);
|
|
49
75
|
return function *(iterator) {
|
|
50
76
|
let i = 0;
|
|
51
77
|
for (let word of iterator) {
|
|
@@ -66,8 +92,11 @@ export function title({} = {}) {
|
|
|
66
92
|
}
|
|
67
93
|
|
|
68
94
|
export function description({
|
|
69
|
-
|
|
70
|
-
|
|
95
|
+
wordsPerSentence = {
|
|
96
|
+
avg: 24,
|
|
97
|
+
std: 5,
|
|
98
|
+
min: 1,
|
|
99
|
+
},
|
|
71
100
|
} = {}) {
|
|
72
101
|
return function *(iterator) {
|
|
73
102
|
let word;
|
|
@@ -79,7 +108,7 @@ export function description({
|
|
|
79
108
|
word = _word;
|
|
80
109
|
if (slen === 0) {
|
|
81
110
|
word = capitalize(word);
|
|
82
|
-
slen =
|
|
111
|
+
slen = gaussMS(wordsPerSentence);
|
|
83
112
|
}
|
|
84
113
|
if (--slen === 0) {
|
|
85
114
|
word += '.';
|
|
@@ -99,8 +128,23 @@ function capitalize(word) {
|
|
|
99
128
|
return word[0].toUpperCase() + word.substring(1);
|
|
100
129
|
}
|
|
101
130
|
|
|
102
|
-
|
|
103
|
-
|
|
131
|
+
// TODO: have a random variable expression
|
|
132
|
+
function gaussMS(args) {
|
|
133
|
+
if (typeof args === 'number') {
|
|
134
|
+
return Math.round(avg);
|
|
135
|
+
}
|
|
136
|
+
let { avg, std, min, max } = args;
|
|
137
|
+
if (std === undefined) {
|
|
138
|
+
std = avg / 4;
|
|
139
|
+
}
|
|
140
|
+
let n = gaussRandom() * std + avg;
|
|
141
|
+
if (min !== undefined) {
|
|
142
|
+
n = Math.max(min, n);
|
|
143
|
+
}
|
|
144
|
+
if (max !== undefined) {
|
|
145
|
+
n = Math.min(max, n);
|
|
146
|
+
}
|
|
147
|
+
return Math.round(n);
|
|
104
148
|
}
|
|
105
149
|
|
|
106
150
|
function gaussRandom() {
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { randomInt, imageUrl, shuffle } from '../utils.js';
|
|
2
|
+
import * as lorem from '../lorem.js';
|
|
3
|
+
|
|
4
|
+
// TODO: wild mode that generates edge cases
|
|
5
|
+
|
|
6
|
+
export function markdown({ features, blocks = [8, 12] } = {}) {
|
|
7
|
+
// TODO: block features
|
|
8
|
+
return [
|
|
9
|
+
atxHeading({ features }),
|
|
10
|
+
paragraph({ features }),
|
|
11
|
+
fencedCodeBlock({ features }),
|
|
12
|
+
paragraph({ features }),
|
|
13
|
+
table({ features }),
|
|
14
|
+
image(),
|
|
15
|
+
paragraph({ features }),
|
|
16
|
+
hr(),
|
|
17
|
+
atxHeading({ features }),
|
|
18
|
+
paragraph({ features }),
|
|
19
|
+
list({ features }),
|
|
20
|
+
paragraph({ features }),
|
|
21
|
+
].join('\n\n');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// leaf blocks //
|
|
25
|
+
export function hr() {
|
|
26
|
+
// wild mode: while spaces at the beginning (< 4), in between, in the end
|
|
27
|
+
// wild mode: no line break for '*'
|
|
28
|
+
return '*-_'.charAt(randomInt(0, 2)).repeat(randomInt(3, 6));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function atxHeading({ features, level = [1, 6], size = [1, 8], content }) {
|
|
32
|
+
const words = content || lorem.lorem({ size });
|
|
33
|
+
return `${'#'.repeat(randomInt(...level))} ${words}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function setextHeading({ features, level = [1, 2], size = [1, 8], content }) {
|
|
37
|
+
const words = content || lorem.lorem({ size });
|
|
38
|
+
return `${words}\n${'=-'.charAt(randomInt(...level) - 1).repeat(3)}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function linkReferenceDefinition({ label, destination, title }) {
|
|
42
|
+
return `[${label}]: ${destination}${title !== undefined ? ` ${title}` : ''}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function indentedCodeBlock({ lang, content, size }) {
|
|
46
|
+
content = content || codeContent({ lang, size });
|
|
47
|
+
return indent(4, content);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function fencedCodeBlock({ lang, content, size, fenceChar = '`' }) {
|
|
51
|
+
if (fenceChar === 'random') {
|
|
52
|
+
fenceChar = '`~'.charAt(randomInt(0, 1));
|
|
53
|
+
}
|
|
54
|
+
content = content || codeContent({ lang, size });
|
|
55
|
+
// TODO: escape fenceChar in content
|
|
56
|
+
return `${fenceChar.repeat(3)}${lang || ''}\n${content}\n${fenceChar.repeat(3)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function paragraph({ features, size = [20, 50] }) {
|
|
60
|
+
return lorem.lorem({ size, decorates: ['description', decorate({ features })] });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function table({ features, columns = [2, 4], rows = [2, 8] }) {
|
|
64
|
+
columns = randomInt(...columns);
|
|
65
|
+
rows = randomInt(...rows);
|
|
66
|
+
const defs = [...multiply({ size: 1 }, columns - 1), { size: [3, 8] }];
|
|
67
|
+
const header = lorem.lorem({ size: columns, output: 'array' });
|
|
68
|
+
const delimiter = defs.map(() => '---');
|
|
69
|
+
const body = [ header, delimiter ];
|
|
70
|
+
for (let i = 0; i < rows - 1; i++) {
|
|
71
|
+
body.push(defs.map(({ size }) => lorem.lorem({ size })));
|
|
72
|
+
}
|
|
73
|
+
return body.map(tableRow).join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function image({ url, imageSize = [400, 250], ...options } = {}) {
|
|
77
|
+
url = url || imageUrl(imageSize);
|
|
78
|
+
return ``;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// container blocks //
|
|
82
|
+
export function blockquote({ features, size = [3, 5] }) {
|
|
83
|
+
// TODO
|
|
84
|
+
return _blockquote(lorem.lorem({ size }));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const LIST_ITEM_TYPES = ['ordered', 'bullet', 'task'];
|
|
88
|
+
|
|
89
|
+
export function list({ features, type = 'random', count = [1, 8], size = [5, 15] }) {
|
|
90
|
+
count = typeof count === 'number' ? count : randomInt(...count);
|
|
91
|
+
const t = type === 'random' ? LIST_ITEM_TYPES[Math.floor(3 * Math.random())] : type;
|
|
92
|
+
const items = [];
|
|
93
|
+
while (count > 0) {
|
|
94
|
+
const c = randomInt(1, count);
|
|
95
|
+
let content = paragraph({ features, size });
|
|
96
|
+
if (c > 1) {
|
|
97
|
+
content += `\n${list({ features, type, count: c - 1, size })}`;
|
|
98
|
+
}
|
|
99
|
+
items.push(listItem(t, content));
|
|
100
|
+
count -= c;
|
|
101
|
+
}
|
|
102
|
+
return items.join('\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// inline //
|
|
106
|
+
export function codeSpan(options) {
|
|
107
|
+
return `\`${_content(options)}\``;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function emphasis({ level = [1, 3], options }) {
|
|
111
|
+
level = typeof level === 'number' ? level : randomInt(...level);
|
|
112
|
+
const str = '_*'.charAt(randomInt(0, 1)).repeat(level);
|
|
113
|
+
return `${str}${_content(options)}${str}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function link({ url = 'https://miso.ai', ...options } = {}) {
|
|
117
|
+
return `[${_content(options)}](${url})`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// TODO: ref link
|
|
121
|
+
// TODO: autolink
|
|
122
|
+
// TODO: hard line break
|
|
123
|
+
|
|
124
|
+
const INLINE_FEATURES = {
|
|
125
|
+
'code-span': () => ['`', '`'],
|
|
126
|
+
'emphasis-1': () => multiply(_emphasisAdfix(1), 2),
|
|
127
|
+
'emphasis-2': () => multiply(_emphasisAdfix(2), 2),
|
|
128
|
+
'emphasis-3': () => multiply(_emphasisAdfix(3), 2),
|
|
129
|
+
'link': ({ url = 'https://miso.ai' } = {}) => [`[`, `](${url})`],
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
function _emphasisAdfix(level = [1, 3]) {
|
|
133
|
+
level = typeof level === 'number' ? level : randomInt(...level);
|
|
134
|
+
return '_*'.charAt(randomInt(0, 1)).repeat(level);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const INLINE_FEATURE_LIST = Object.keys(INLINE_FEATURES);
|
|
138
|
+
const INLINE_FEATURE_SET = new Set(INLINE_FEATURE_LIST);
|
|
139
|
+
|
|
140
|
+
// decorator //
|
|
141
|
+
export function decorate({ features = INLINE_FEATURE_LIST, size = [1, 3], rest = [0, 8] } = {}) {
|
|
142
|
+
features = features.filter(f => INLINE_FEATURE_SET.has(f));
|
|
143
|
+
|
|
144
|
+
const unused = shuffle([...features]);
|
|
145
|
+
let unusedCursor = unused.length - 1;
|
|
146
|
+
|
|
147
|
+
const rollRest = () => typeof rest === 'number' ? rest : randomInt(...rest);
|
|
148
|
+
const rollFeatureSize = () => typeof size === 'number' ? size : randomInt(...size);
|
|
149
|
+
const rollFeatureType = () => unusedCursor >= 0 ? unused[unusedCursor--] : features[randomInt(0, features.length - 1)];
|
|
150
|
+
|
|
151
|
+
return function *(iterator) {
|
|
152
|
+
let count = rollRest();
|
|
153
|
+
let suffix;
|
|
154
|
+
let lastWord;
|
|
155
|
+
for (const word of iterator) {
|
|
156
|
+
if (lastWord) {
|
|
157
|
+
yield lastWord;
|
|
158
|
+
}
|
|
159
|
+
if (count === 0) {
|
|
160
|
+
if (suffix) {
|
|
161
|
+
lastWord = `${word}${suffix}`;
|
|
162
|
+
suffix = undefined;
|
|
163
|
+
count = rollRest();
|
|
164
|
+
} else {
|
|
165
|
+
const [prefix, s] = INLINE_FEATURES[rollFeatureType()]();
|
|
166
|
+
lastWord = `${prefix}${word}`;
|
|
167
|
+
suffix = s;
|
|
168
|
+
count = rollFeatureSize();
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
lastWord = word;
|
|
172
|
+
count--;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (lastWord) {
|
|
176
|
+
yield suffix !== undefined ? `${lastWord}${suffix}` : lastWord;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// helper //
|
|
182
|
+
function _content({ size = [1, 3], content } = {}) {
|
|
183
|
+
return content || lorem.lorem({ size });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function _blockquote(content) {
|
|
187
|
+
return content.split('\n').map(line => `> ${line}`).join('\n');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function indent(size, content) {
|
|
191
|
+
return content.split('\n').map(line => ' '.repeat(size) + line).join('\n');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function listItem(type, content) {
|
|
195
|
+
const [firstLine, restLines] = content.split('\n', 2);
|
|
196
|
+
const result = `${listItemPrefix(type)} ${firstLine}`;
|
|
197
|
+
const indentSize = type === 'ordered' ? 3 : 2;
|
|
198
|
+
return !restLines ? result : result + `\n${indent(indentSize, restLines)}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function listItemPrefix(type, checked = 'random') {
|
|
202
|
+
switch (type) {
|
|
203
|
+
case 'ordered':
|
|
204
|
+
return '1.';
|
|
205
|
+
case 'bullet':
|
|
206
|
+
return '-';
|
|
207
|
+
case 'task':
|
|
208
|
+
const mark = (checked === 'random' ? Math.random() < 0.5 : !!checked) ? 'x' : ' ';
|
|
209
|
+
return `- [${mark}]`;
|
|
210
|
+
default:
|
|
211
|
+
throw new Error(`unknown list item type: ${type}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function codeContent({ lang, size = [10, 30] }) {
|
|
216
|
+
// TODO
|
|
217
|
+
switch (lang) {
|
|
218
|
+
default:
|
|
219
|
+
return lorem.lorem({ output: 'multiline', size });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function tableRow(cells) {
|
|
224
|
+
return `| ${cells.join(' | ')} |`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function multiply(obj, i) {
|
|
228
|
+
const arr = [];
|
|
229
|
+
for (let j = 0; j < i; j++) {
|
|
230
|
+
arr.push(typeof obj === 'function' ? obj() : obj);
|
|
231
|
+
}
|
|
232
|
+
return arr;
|
|
233
|
+
}
|
package/src/data/products.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as u from './utils.js';
|
|
2
|
-
import * as
|
|
2
|
+
import * as fields from './fields.js';
|
|
3
3
|
|
|
4
4
|
export function *products({ rows, ...options } = {}) {
|
|
5
5
|
for (let i = 0; i < rows; i++) {
|
|
@@ -9,43 +9,22 @@ export function *products({ rows, ...options } = {}) {
|
|
|
9
9
|
|
|
10
10
|
function product({} = {}) {
|
|
11
11
|
const id = u.id();
|
|
12
|
-
const prices = u.repeat(
|
|
12
|
+
const prices = u.repeat(fields.price, [1, 2]);
|
|
13
13
|
prices.sort();
|
|
14
|
-
const seed = Math.floor(Math.random() * 1000);
|
|
15
14
|
|
|
16
15
|
return {
|
|
17
16
|
product_id: id,
|
|
18
|
-
authors:
|
|
19
|
-
min: 1,
|
|
20
|
-
max: 3,
|
|
21
|
-
fixedStarts: 0,
|
|
22
|
-
decorates: ['title'],
|
|
23
|
-
output: 'array',
|
|
24
|
-
}),
|
|
17
|
+
authors: fields.authors(),
|
|
25
18
|
categories: [],
|
|
26
|
-
tags:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
fixedStarts: 0,
|
|
30
|
-
output: 'array',
|
|
31
|
-
}),
|
|
32
|
-
title: lorem.lorem({
|
|
33
|
-
min: 2,
|
|
34
|
-
max: 6,
|
|
35
|
-
fixedStarts: 0,
|
|
36
|
-
decorates: ['title'],
|
|
37
|
-
}),
|
|
38
|
-
description: lorem.lorem({
|
|
39
|
-
min: 10,
|
|
40
|
-
max: 20,
|
|
41
|
-
decorates: ['description'],
|
|
42
|
-
}),
|
|
19
|
+
tags: fields.tags(),
|
|
20
|
+
title: fields.title(),
|
|
21
|
+
description: fields.description(),
|
|
43
22
|
//html,
|
|
44
|
-
cover_image:
|
|
23
|
+
cover_image: fields.image(),
|
|
45
24
|
url: `/products/${id}`,
|
|
46
25
|
sale_price: prices[0],
|
|
47
26
|
original_price: prices[prices.length - 1],
|
|
48
|
-
rating:
|
|
49
|
-
availability:
|
|
27
|
+
rating: fields.rating(),
|
|
28
|
+
availability: fields.availability(),
|
|
50
29
|
};
|
|
51
30
|
}
|
package/src/data/utils.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export function randomInt(min, max) {
|
|
2
|
-
return max
|
|
2
|
+
return max == null || (max <= min) ? min : (min + Math.floor(Math.random() * (max - min)));
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// TODO: pass in size
|
|
6
|
+
export function repeat(fn, range) {
|
|
7
|
+
const n = randomInt(...range);
|
|
7
8
|
const result = [];
|
|
8
9
|
for (let i = 0; i < n; i++) {
|
|
9
10
|
result.push(fn());
|
|
@@ -15,14 +16,16 @@ export function id() {
|
|
|
15
16
|
return Math.random().toString(36).substring(2, 10);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
export function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return
|
|
19
|
+
export function shuffle(array) {
|
|
20
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
21
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
22
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
23
|
+
}
|
|
24
|
+
return array;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export function
|
|
27
|
-
|
|
27
|
+
export function imageUrl(size) {
|
|
28
|
+
const seed = Math.floor(Math.random() * 1000);
|
|
29
|
+
const sizePath = Array.isArray(size) ? size.length > 1 ? `${size[0]}/${size[1]}` : `${size[0]}` : `${size}`;
|
|
30
|
+
return `https://picsum.photos/seed/${seed}/${sizePath}`;
|
|
28
31
|
}
|
package/src/index.js
CHANGED
|
@@ -3,12 +3,13 @@ import Router from '@koa/router';
|
|
|
3
3
|
import cors from '@koa/cors';
|
|
4
4
|
import serveStatic from 'koa-static';
|
|
5
5
|
import { koaBody } from 'koa-body';
|
|
6
|
-
import
|
|
6
|
+
import _api from './api/index.js';
|
|
7
7
|
import { exclusion } from './utils.js';
|
|
8
8
|
|
|
9
|
-
export default function doggoganger({ port = 9901, serve = false } = {}) {
|
|
9
|
+
export default function doggoganger({ port = 9901, serve = false, ...options } = {}) {
|
|
10
10
|
const app = new Koa();
|
|
11
11
|
const router = new Router();
|
|
12
|
+
const api = _api(options);
|
|
12
13
|
|
|
13
14
|
router.use('/api', api.routes(), api.allowedMethods());
|
|
14
15
|
|